package jmine.tec.web.wicket.pages.form;

import static jmine.tec.web.wicket.pages.form.WebFormPagesMessages.INFO_ENTITY_SAVE_SUCCESS;
import static jmine.tec.web.wicket.pages.form.WebFormPagesMessages.LABEL_FORM_TITLE;

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import jmine.tec.persist.PersistMessages;
import jmine.tec.persist.hibernate.initializer.ProxyInitializer;
import jmine.tec.persist.hibernate.merger.EntityMerger;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;
import jmine.tec.web.wicket.component.command.button.BackCommand;
import jmine.tec.web.wicket.component.command.button.BackCommandHandler;
import jmine.tec.web.wicket.component.command.button.ButtonCommand;
import jmine.tec.web.wicket.component.command.button.ButtonCommandsPanel;
import jmine.tec.web.wicket.component.command.button.SaveCommand;
import jmine.tec.web.wicket.component.command.button.SaveCommandHandler;
import jmine.tec.web.wicket.pages.HistoryAwarePage;
import jmine.tec.web.wicket.spring.SpringBeanReference;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.IPageFactory;
import org.apache.wicket.Page;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.validation.IFormValidator;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Implementação base de uma página de inclusão/visualização e edição de entidades.
 * 
 * @author Rafael Volpato
 * @date Aug 8, 2011
 * @param <BO> tipo da entidade
 */
public abstract class FormPage<BO extends Persistable> extends AbstractFormPage implements HistoryAwarePage, BackCommandHandler,
        SaveCommandHandler {

    private static final String SOURCE = "sourcePageClass";

    private final SpringBeanReference<BaseDAOFactory> daoFactoryRef = SpringBeanReference.forName("daoFactory");

    private final SpringBeanReference<EntityMerger> entityMerger = SpringBeanReference.forName("hibernateEntityMerger");

    private final SpringBeanReference<ProxyInitializer> proxyInitializer = SpringBeanReference.forName("hibernateProxyInitializer");

    private final FormType formType;

    private Class<? extends Page> source;

    private Page backToPage;

    private Form<BO> form;

    private WebMarkupContainer fieldset;

    private IModel<BO> model;
    
    /**
     * Construtor para a criação de uma nova entidade.
     * 
     * @param pageParameters {@link PageParameters}
     */
    public FormPage(PageParameters pageParameters) {
        super(pageParameters);
        this.formType = FormType.NEW;
        this.source = this.getSourceClass(pageParameters);
        this.model = new CompoundPropertyModel<BO>(this.createEntity());
    }

    /**
     * Construtor para a criação de uma nova entidade.
     * 
     * @param pageParameters {@link PageParameters}
     * @param pageInstance the source page instance
     */
    public FormPage(PageParameters pageParameters, Page pageInstance) {
        super(pageParameters);
        this.formType = FormType.NEW;
        this.backToPage = pageInstance;
        this.model = new CompoundPropertyModel<BO>(this.createEntity());
    }

    /**
     * Construtor para a criação de uma nova entidade.
     * 
     * @param sourcePageParameters {@link PageParameters}
     * @param source página de origem
     */
    public FormPage(Class<? extends Page> source, PageParameters sourcePageParameters) {
        super(sourcePageParameters);
        this.formType = FormType.NEW;
        this.source = source;
        this.model = new CompoundPropertyModel<BO>(this.createEntity());
    }

    /**
     * Construtor.
     * 
     * @param source página de origem
     * @param sourcePageParameters page parameters da pagina de origem
     * @param entity entidade que deve ser associado a página
     * @param formType tipo do formulario da página
     */
    public FormPage(Class<? extends Page> source, PageParameters sourcePageParameters, BO entity, FormType formType) {
        super(sourcePageParameters);
        this.source = source;
        this.formType = formType;
        this.initializeProxy(entity);
        this.model = new CompoundPropertyModel<BO>(entity);
    }

    /**
     * Construtor.
     * 
     * @param sourcePage página de origem
     * @param sourcePageParameters page parameters da pagina de origem
     * @param entity entidade que deve ser associado a página
     * @param formType tipo do formulario da página
     */
    public FormPage(Page sourcePage, PageParameters sourcePageParameters, BO entity, FormType formType) {
        super(sourcePageParameters);
        this.backToPage = sourcePage;
        this.formType = formType;
        this.initializeProxy(entity);
        this.model = new CompoundPropertyModel<BO>(entity);
    }

    /**
     * Força a inicialização de campos lazy que serão necessários durante a utilização da tela.
     * 
     * @param entity entity
     */
    protected void initializeProxy(BO entity) {
        this.getProxyInitializer().initialize(entity);
    }

    /**
     * @return obtém o inicializador de proxies
     */
    protected ProxyInitializer getProxyInitializer() {
        return this.proxyInitializer.get();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onInitialize() {
        super.onInitialize();
        this.createComponents();
    }
    
    /**
     * Cria os componentes da tela
     */
    private void createComponents() {
        this.form = new Form<BO>("mainForm");
        this.form.setModel(this.model);
        this.add(new FeedbackPanel("feedback").setOutputMarkupId(true));

        if (this.formType.isView()) {
            this.add(new Label("formTitle", this.getPageName()));
        } else {
            this.add(new Label("formTitle", LABEL_FORM_TITLE.create(this.getPageName()).getMessage()));
        }

        this.form.add(new ButtonCommandsPanel("pageCommands", this.getPageCommands()));

        this.add(this.form);

        this.fieldset = new WebMarkupContainer("mainFieldset");
        this.form.add(this.fieldset);

        List<Component> components = this.createFormComponents();

        boolean disableField = this.formType.isView();

        // For most Form components
        AttributeModifier ro = new AttributeModifier("readonly", disableField, new Model<String>("readonly"));
        // For DropDowns etc.
        AttributeModifier disabled = new AttributeModifier("disabled", disableField, new Model<String>("disabled"));

        for (Component component : components) {
            // se o componente ja foi desabilitado propositalmente, nao sera
            // habilitado
            this.disableComponent(disableField, ro, disabled, component);
            this.fieldset.add(component);
        }

        if (!this.formType.isView()) {
            for (IFormValidator validator : this.createFormValidators()) {
                this.form.add(validator);
            }
        }
    }

    /**
     * @param disableField deve desabilitar?
     * @param ro readonly tag
     * @param disabled disabled tag
     * @param component componente
     */
    protected void disableComponent(boolean disableField, AttributeModifier ro, AttributeModifier disabled, Component component) {
        // hook
        if (component.isEnabled()) {
            component.add(ro).add(disabled).setEnabled(!disableField);
        }
    }

    /**
     * Salva a entidade.<br>
     * Este método é disparado pelo evento ao pressionar botão de gravar.
     */
    public void save() {
        BO target = FormPage.this.getMergedEntity();
        if (!FormPage.this.beforeSave(target)) {
            return;
        }
        target.getPersister().save();

        // Se o cadastro for de edição, apos salvar direcionar para a página de
        // pesquisa
        this.redirectAfterSave();
        // não queremos isso =)
        this.getSession().info(INFO_ENTITY_SAVE_SUCCESS.create().getMessage());
    }

    /**
     * Calls {@link #newPageInstance()} and sets the backTo attributes
     * 
     * @return a {@link FormPage} that has the same class as the current {@link FormPage}
     */
    protected final FormPage<?> restartCurrentPage() {
        FormPage<?> page = this.newPageInstance();
        page.setBackToPageInstace(this.backToPage);
        page.source = this.source;
        return page;
    }

    /**
     * Creates a new {@link FormPage} that must have the same class as the current {@link FormPage}. Currently it delegates to the
     * {@link IPageFactory}, which by default uses reflection. Subclasses may want to override this method and plainly call new
     * 
     * @return the {@link FormPage}
     */
    protected FormPage<?> newPageInstance() {
        RequestCycle requestCycle = RequestCycle.get();
        IPageFactory pageFactory = requestCycle.getApplication().getSessionSettings().getPageFactory();
        final Map<String, String[]> requestMap = requestCycle.getRequest().getParameterMap();
        requestMap.putAll(this.getPageParameters().toRequestParameters());
        return (FormPage) pageFactory.newPage(this.getClass(), new PageParameters(requestMap));
    }

    /**
     * Hook para manipular a entidade a ser salva, antes de executar o save propriamente dito.
     * 
     * @param target target
     * @return <code>true</code> se a entidade deve ser salva, <code>false</code> caso contrario
     */
    protected boolean beforeSave(BO target) {
        // hook
        return true;
    }

    /**
     * Retorna a classe da entidade que esta sendo manipulada por esta página.
     * 
     * @return a classe da entidade que esta sendo manipulada por esta página
     */
    @SuppressWarnings("unchecked")
    private Class<BO> getCRUDClass() {
        try {
            return (Class<BO>) GenericTypeResolverImpl.getInstance().resolveTypeForParameterizedType(
                    (ParameterizedType) this.getClass().getGenericSuperclass(), this.getClass(), 0);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.INVALID_BEAN_IMPLEMENTATION.create(this.getClass()));
        }
    }

    /**
     * Extrai a classe da página de origem a partir do {@link PageParameters}
     * 
     * @param pageParameters {@link PageParameters}
     * @return a classe da página de origem
     */
    @SuppressWarnings("unchecked")
    private Class<? extends Page> getSourceClass(PageParameters pageParameters) {
        String sourceClass = pageParameters.getString(SOURCE);
        if (sourceClass == null) {
            return this.getClass();
        }
        try {
            return (Class<? extends Page>) ReflectionUtils.findClass(sourceClass);
        } catch (ClassCastException e) {
            return this.getClass();
        } catch (ClassNotFoundException e) {
            return this.getClass();
        }
    }

    /**
     * Define a entidade associada ao formulário.
     * 
     * @param entity a nova entidade para o formulário
     */
    protected final void setEntity(BO entity) {
        if (entity == null) {
            throw new IllegalArgumentException("entity must be != null");
        }
        this.form.setModelObject(entity);
        this.initializeProxy(entity);
    }

    /**
     * Retorna a entidade que esta sendo manipulada pelo formulario da página.
     * 
     * @return a entidade que esta sendo manipulada pelo formulario da página
     */
    protected final BO getEntity() {
        return this.form.getModelObject();
    }

    /**
     * Obtém uma cópia da entidade do model com as mudanças feitas, associada à session e pronta para ser salva.
     * 
     * @return merged entity
     */
    protected BO getMergedEntity() {
        return this.getEntityMerger().merge(this.getEntity());
    }

    /**
     * Obtém o merger de entidades
     * 
     * @return entity merger
     */
    protected EntityMerger getEntityMerger() {
        return this.entityMerger.get();
    }

    /**
     * @return the {@link FormType}
     */
    protected final FormType getFormType() {
        return this.formType;
    }

    /**
     * @return fieldset o fieldset principal
     */
    public final WebMarkupContainer getFieldset() {
        return this.fieldset;
    }

    /**
     * Cria uma nova instância da entidade.
     * 
     * @return uma nova instancia da entidade
     */
    protected BO createEntity() {
        BaseDAO<BO> dao = this.daoFactoryRef.get().getGenericDAO(this.getCRUDClass());
        return dao.createBean();
    }

    /**
     * Retorna a lista de componentes que devem ser adicionados no formulário.
     * 
     * @return a lista de componentes que devem ser adicionados no formulário
     */
    protected abstract List<Component> createFormComponents();

    /**
     * Retorna {@link BaseDAOFactory} para ser utilizado pelas telas que estendem {@link FormPage}
     * 
     * @return {@link BaseDAOFactory} para ser utilizado pelas telas que estendem {@link FormPage}
     */
    protected BaseDAOFactory getDAOFactory() {
        return this.daoFactoryRef.get();
    }

    /**
     * @param sourcePageInstance the sourcePageInstance to set
     */
    public void setBackToPageInstace(Page sourcePageInstance) {
        this.backToPage = sourcePageInstance;
    }

    /**
     * This method is called after a successfull save is performed and redirects the user
     */
    protected void redirectAfterSave() {
        if (FormPage.this.formType.isEdit()) {
            this.back();
        } else {
            // Se for um novo cadastro, reiniciar a pagina. Mas se apertar o
            // botao voltar, deve manter o source
            FormPage<?> page = this.restartCurrentPage();
            this.setResponsePage(page);
        }
    }

    /**
     * Redirects the user to the previous page
     * 
     * @return null
     */
    public Page back() {
        if (this.backToPage != null) {
            this.setResponsePage(FormPage.this.backToPage);
        } else if (this.source != null) {
            this.setResponsePage(FormPage.this.source, this.getPageParameters());
        }
        return null;
    }

    /**
     * Retorna a lista de comandos que deve ser exibidos junto com a tabela de entidade encontradas.
     * 
     * @return lista de comandos que devem ser exibidos
     */
    protected List<ButtonCommand> getPageCommands() {
        List<ButtonCommand> commands = new ArrayList<ButtonCommand>();
        if (!this.getFormType().isView()) {
            commands.add(new SaveCommand(this));
        }
        commands.add(new BackCommand(this));
        return commands;
    }
    
    /**
     * Validators to be added in this page form.
     * 
     * @return a list of IFormValidator
     */
    protected List<IFormValidator> createFormValidators() {
        return Collections.emptyList();
    }
}
